一覧に戻る

MapLibre GL JSをLeafletとの違いに注目して紹介する

#TypeScript#leaflet#foss4g#MapLibre

MapLibre User Group Japanの@Kanahiroです。今回はMapLibre GL JSを紹介します。

MapLibre GL JSとは

https://github.com/maplibre/maplibre-gl-js

WebGLとベクトルタイルを背景とした、ブラウザ向け地図ライブラリです。

MapLibre GL JSの紹介は既にいくつも記事が公開されています。

https://qiita.com/Shogo_Hirasawa/items/2c219a13304580eb70b4

https://qiita.com/asahina820/items/66cd78a4462db86578a4

なので本記事では、最もポピュラーと思われる地図ライブラリであるLeafletとの違いに注目して、GL JSを紹介していきます。

Leaflet

https://leafletjs.com/

とてもシンプルですが、背景に地図タイルを表示して・ピンを立てて・簡単な図形を描画して…、という地図にまつわるよくあるニーズには大体対応できます。学習コストが低く、現在でも最もよく使われている地図ライブラリです。

npm trends

https://npmtrends.com/leaflet-vs-mapbox-gl-vs-maplibre-gl-vs-ol

Mapbox GL JS(mapbox-gl)が未だに強いのはさすが。MapLibre GL JSは思ったより伸び代がある。

MapLibre GL JSをLeafletとの違いに注目して紹介

先に述べた「地図にまつわるよくあるニーズ」はMapLibre GL JSでも間違いなく実装可能です。そのうえでMapLibre GL JSのユニークな点を紹介していきます。

:::note Leafletの作者Volodymyr Agafonkin氏は、その後Mapbox GL JSを開発して、今も活発にメンテナンスされています。 :::

描画パフォーマンス

LeafletはCanvasで描画しているので、WebGLで動作しているGL JSに比べて、ベクター図形の描画パフォーマンスが低いです。

https://qiita.com/dayjournal/items/86e1ca065e03ea88e0fb#mapbox-gl-jsleafletopenlayers%E3%81%A7%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E8%A1%A8%E7%A4%BA%E6%95%B0%E3%81%AE%E9%99%90%E7%95%8C%E3%82%92%E6%8E%A2%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%BE%E3%81%97%E3%81%9F-thumbsup

地図ライブラリ間の比較は上記の記事が詳しく、Leafletは5万個のポイントの描画が厳しい一方、GL JSなら50万個くらいまでは問題なく描画出来るという結果です。

ベクトルタイルのサポート

MapLibre GL JSはベクトルタイルをネイティブでサポートしていますが、Leafletはそうではありません。なので描画パフォーマンスとは別に、大規模な位置情報データを受け取って表示することが出来ません。

:::note フォーク元のMapbox GL JSがそもそもベクトルタイルをレンダリングするためのライブラリだということです(なのでベクトルタイルをサポートしてて当たり前)。 :::

地図スタイリング

https://maplibre.org/maplibre-gl-js/docs/examples/3d-buildings/

地図のスタイリング(色、ラベル、etc)機能が、GL JSの方が遥かに高度で柔軟です。ただしその能力を発揮するためにはStyle Specificationを理解する必要があります。この点でGL JSは学習コストが若干高いです。

Style Specificationは以下のようなJSONから成ります。

https://tile.openstreetmap.jp/styles/maptiler-toner-ja/style.json から抜粋

{
  "version": 8,
  "center": [
    -122.41877447993727,
    37.7977350127602
  ],
  "zoom": 10.426085190067841,
  "bearing": 0,
  "pitch": 0,
  "sources": {
    "openmaptiles": {
      "type": "vector",
      "url": "https://tile.openstreetmap.jp/data/planet.json"
    }
  },
  "sprite": "https://tile.openstreetmap.jp/styles/maptiler-toner-ja/sprite",
  "glyphs": "https://tile.openstreetmap.jp/fonts/{fontstack}/{range}.pbf",
  "layers": [
    {
      "id": "background",
      "paint": {
        "background-color": "#fff"
      },
      "type": "background"
    },
    {
      "filter": [
        "==",
        "class",
        "grass"
      ],
      "id": "landcover_grass_fill",
      "metadata": {
        "mapbox:group": "1444849388993.3071"
      },
      "minzoom": 10,
      "paint": {
        "fill-antialias": true,
        "fill-color": {
          "stops": [
            [
              10,
              "rgba(0, 0, 0, 0.3)"
            ],
            [
              16,
              "rgba(0, 0, 0, 1)"
            ]
          ]
        },
        "fill-opacity": 1,
        "fill-outline-color": "rgba(0, 0, 0, 0)"
      },
      "source": "openmaptiles",
      "source-layer": "landcover",
      "type": "fill"
    },
    {
      "filter": [
        "all",
        [
          "==",
          "$type",
          "LineString"
        ],
        [
          "!in",
          "class",
          "pier",
          "path",
          "rail"
        ],
        [
          "in",
          "class",
          "motorway",
          "trunk"
        ]
      ],
      "id": "road_highway",
      "layout": {
        "line-cap": "round",
        "line-join": "round",
        "visibility": "visible"
      },
      "metadata": {
        
      },
      "minzoom": 6,
      "paint": {
        "line-color": {
          "stops": [
            [
              6,
              "rgba(0, 0, 0, 0.02)"
            ],
            [
              10,
              "rgba(0, 0, 0, 0.6)"
            ],
            [
              16,
              "rgba(0, 0, 0, 1)"
            ]
          ]
        },
        "line-opacity": 1,
        "line-width": {
          "stops": [
            [
              7,
              1
            ],
            [
              10,
              2
            ],
            [
              16,
              8
            ]
          ]
        }
      },
      "source": "openmaptiles",
      "source-layer": "transportation",
      "type": "line"
    }
  ]
}

上記の(抜粋前の)「style.json」は、以下のような地図デザインを意味します。

https://tile.openstreetmap.jp/styles/maptiler-toner-ja/#9.44/36.1132/139.7008/0/60

提供されているAPIの使い方

これまでは技術的な側面の議論に終始してきましたが、ここでテクニカルな話をひとつ。 ウェブアプリケーションを実装する中で、「レイヤー」を追加したり削除したりというのは良くある話です。そのAPIに、LeafletとGL JSの大きな違いがあります。

Leafletの場合

// マップを初期化。mapというIDを持つDiv要素があるとして:
const mymap = L.map('map').setView([35.681236, 139.767125], 13); // 東京駅の座標で初期化

// OpenStreetMapを背景地図として追加
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap contributors'
}).addTo(mymap);

Mapを定義して、TileLayerを定義して、Mapインスタンスに追加…という手続き的なAPIが提供されています。

MapLibre GL JSの場合

const map = new maplibregl.Map({
    container: 'map', // 地図を表示するコンテナのID
    style: {
        version: 8,
        sources: {
            'raster-tiles': {
                type: 'raster',
                tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
                tileSize: 256,
                attribution: '© OpenStreetMap contributors'
            }
        },
        layers: [{
            id: 'simple-tiles',
            type: 'raster',
            source: 'raster-tiles',
            minzoom: 0,
            maxzoom: 22
        }]
    },
    center: [139.767125, 35.681236], // 初期位置を東京駅周辺に設定
    zoom: 13 // 初期ズームレベル
});

表示すべきレイヤーはstyleとして宣言的に定義出来るようになっています。

先に言及したとおり、MapLibre GL JSではStyle Specificationに基づいて地図が描画されます。Style Specificationは一定の構造のJSONであり、すなわち宣言的です。ここがLeafletとの大きな違いであり、たとえ同じ地図を表示するだけであっても、実装のシンプルさは長期的にはGL JSが勝るでしょう(レイヤーが2個3個と増えていくことを考えましょう)。

現代のWeb開発では何かしらのフレームワークを用いることが当たり前になっていますが、宣言的なAPIの方が嬉しいことは多くの方にご理解いただけると思います。GL JSでは、map.setStyle()という関数が提供されているので、**styleを更新してsetStyle()**というパターンを利用することで、極めて宣言的なコードを書くことが可能になります。

MapLibre Nativeでも応用が効く

スマホなど向けのライブラリであるMapLibre NativeでもStyle Specificationは共通ですし、APIも似通っています。なのでGL JSを学習しておくと、スマホでの実装時の学習コストがとても小さく成ります。

終わりに

パフォーマンスと技術の両面で、Leafletとの比較を行いました。Leafletも素晴らしいライブラリですが、そろそろ移行してもよい時期なんじゃないかと思いますよ!